﻿using System;
using Microsoft.Xna.Framework;
using RaptorEngine.Threads;

namespace RaptorEngine
{

	public class Camera
	{
		//Camera constants
		/// <summary>The id of a camera that has not been added to a scene</summary>
		internal const int NOT_ADDED = -1;

		/// <summary>The id of the debug camera</summary>
		internal const int DEBUG_CAM = -2;

		private int _id = NOT_ADDED;

		internal int Id
		{
			get { return _id; }
			set { _id = value; }
		}

		
		// We only need one Random object no matter how many Cameras we have
		private static readonly Random random = new Random();

		// Are we shaking?
		private bool shaking;

		// The maximum magnitude of our shake offset
		private float shakeMagnitude;

		// The total duration of the current shake
		private float shakeDuration;

		// A timer that determines how far into our shake we are
		private float shakeTimer;

		// The shake offset vector
		private Vector3 shakeOffset;
		private Vector3 _shakePosition;
		private Vector3 _shakeCameraTarget;

		protected Vector3 _cameraTarget;

		private BoundingFrustum _boundingFrustum;

		/// <summary>The data used by the draw-thread</summary>
		internal Matrix projection = Matrix.Identity;

		/// <summary>The data used by the update-thread</summary>
		private Matrix _projection = Matrix.Identity;

		/// <summary>Directly sets the data used by the update-thread
		/// and sends a change message to update the render-thread</summary>
		public Matrix Projection
		{
			get { return _projection; }
			set
			{
				_projection = value;
				//if not added to a scene, don't send a change message
				if (_id != NOT_ADDED)
				{
					GameEngine.Instance.UpdateBuffer.Add(new ChangeMessage()
						{
							MessageType = ChangeMessageType.UpdateCameraProjection,
							ID = Id,
							CameraProjection = value
						});
				}
				SetBoundingFrustum();
			}
		}

		/// <summary>The data used by the draw-thread</summary>
		internal Matrix view = Matrix.Identity;

		/// <summary>The data used by the update-thread</summary>
		private Matrix _view = Matrix.Identity;

		/// <summary>Directly sets the data used by the update-thread
		/// and sends a change message to update the render-thread</summary>
		public Matrix View
		{
			get { return _view; }
			set
			{
				//if the value actually has changed, update it
				if (!_view.Equals(value))
				{
					_view = value;
					//if not added to a scene, don't send a change message
					if (_id != NOT_ADDED)
					{
						GameEngine.Instance.UpdateBuffer.Add(new ChangeMessage()
							{
								MessageType = ChangeMessageType.UpdateCameraView,
								ID = Id,
								CameraViewMatrix = value
							});
					}
				}
				SetBoundingFrustum();

			}
		}

		private Vector3 _position;

		public Vector3 Position
		{
			get { return _position; }

			set
			{
				_position = value;
				View = Matrix.CreateLookAt(_position, _cameraTarget, Vector3.Up);
				SetBoundingFrustum();
			}
		}

		public Camera(Vector3 cameraTarget, Vector3 position)
		{
			_position = position;
			//_position = new Vector3(15f,12,0);
			_cameraTarget = cameraTarget;
			_projection = Matrix.CreatePerspectiveFieldOfView((float) MathHelper.PiOver4,
			                                                  GameEngine.Instance.Render.GraphicsDevice.Viewport.AspectRatio,
			                                                  0.01f,
			                                                  10000f);
			projection = _projection;
			_view = Matrix.CreateLookAt(_position, _cameraTarget, Vector3.Up);
			view = _view;
			_boundingFrustum = new BoundingFrustum(_view * _projection);
			_shakePosition = position;
			_shakeCameraTarget = _cameraTarget;

		}

		public bool InView(BoundingBox boundingBox)
		{
			return _boundingFrustum.Contains(boundingBox) != ContainmentType.Disjoint;
		}

		public bool InView(BoundingSphere boundingSphere)
		{
			return _boundingFrustum.Contains(boundingSphere) != ContainmentType.Disjoint;
		}

		private void SetBoundingFrustum()
		{
			_boundingFrustum.Matrix = _view*_projection;
		}

		/// <summary>
		/// Shakes the camera with a specific magnitude and duration.
		/// </summary>
		/// <param name="magnitude">The largest magnitude to apply to the shake.</param>
		/// <param name="duration">The length of time (in seconds) for which the shake should occur.</param
		public void Shake(float magnitude, float duration)
		{
			shaking = true;

			shakeMagnitude = magnitude;
			shakeDuration = duration;

			// Reset the timer
			shakeTimer = 0f;
		}
		
		/// <summary>
		/// Helper to generate a random float in the range of [-1, 1].
		/// </summary>
		private float NextFloat()
		{
			return (float)random.NextDouble() * 2f - 1f;
		}

		public void Update()
		{
			if (shaking)
			{
				// Move our timer ahead based on the elapsed time
				shakeTimer += 0.1f;
				// If we're at the max duration, we're not going to be shaking anymore
				if (shakeTimer >= shakeDuration)
				{
					shaking = false;
					shakeTimer = shakeDuration;
					_shakePosition = Position;
					_shakeCameraTarget = _cameraTarget;
				}

				// Compute our progress in a [0, 1] range
				float magnitude = (1 - (shakeTimer / shakeDuration)) * shakeMagnitude;

				// Compute our magnitude based on our maximum value and our progress. This causes
				// the shake to reduce in magnitude as time moves on, giving us a smooth transition
				// back to being stationary. We use progress * progress to have a non-linear fall 
				// off of our magnitude. We could switch that with just progress if we want a linear 
				// fall off.
				//float magnitude = shakeMagnitude*(1f - (progress*progress));

				// Generate a new offset vector with three random values and our magnitude
				shakeOffset = new Vector3(NextFloat(), NextFloat(), NextFloat())*magnitude;

				// If we're shaking, add our offset to our position and target
				_shakePosition += shakeOffset;
				_shakeCameraTarget += shakeOffset;

				View = Matrix.CreateLookAt(_shakePosition, _cameraTarget, Vector3.Up);

				_shakePosition = Position;
				_shakeCameraTarget = _cameraTarget;
			}
		}
	}
}
